Java泛型使用与浅析

泛型是Java的一个重要特性,在Java和其他主流语言中有广泛的应用,了解Java泛型有助于加深我们对Java的理解。

What

Java是一种强类型的语言,在强类型的约束下,泛型(Generic Type)允许类、接口或方法中的某些类型在定义时“暂不确定”,在调用时作为参数传入才确定。泛型允许类型作为接口、类或方法的一个参数,这点与C++中的template相似。

Why

为什么要用泛型?主要的目的是为了防止类型转换出错,允许类型作为参数传入。
让我们先看看官方的例子。

1
2
3
4
5
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}

对于Box类的object成员变量,由于该变量是Object类型的,因此可以传入或强制转换为任意类型的对象,在编译期无法预知类型转换的错误,在运行时则会报错。

1
2
3
Box box = new Box();
box.set(new String("Hello World"));
Integer i = (Integer)box.get(); // class cast error

如果将其设置为泛型:

1
2
3
4
5
public class Box <T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}

这样每个Box对象都能使用不同类型的成员变量,但每个具体对象的成员变量类型只有一种。

再来看一个典型的例子。

1
2
3
4
5
6
7
8
List l = new ArrayList();
l.add("Hello World");
l.add(1);

for(int i = 0; i< l.size();i++){
String item = (String) l.get(i); // class cast error
Log.d(item);
}

上述例子中,同样会在运行期由于ArrayList以String类型取Integer类型的元素时,类型转换会失败,而在编译器不会报错。
如果使用泛型的话,编译器在编译时就会报错,避免了运行时的错误。

1
2
3
List<String> l = new ArrayList<>();
l.add("Hello World");
l.add(1); // 编译报错

泛型类与接口

泛型类的使用比较简单,在实例化时确定泛型的类型,泛型标识加在类名或接口名后,如上例:

1
2
3
4
5
public class Box <T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}

泛型标识可以为任意非关键字,通常使用单个大写字符,如T,E等。

泛型接口的使用也大同小异:

1
2
3
4
public interface IBox <T> {
public void set(T t);
public T get();
}

也可以定义多个泛型标识:

1
2
3
4
5
public interface IBox <T, E> {
public void set(T t, E e);
public T getT();
public E getE();
}

此外从上述代码中也可以看出,类中定义的泛型类可直接用于类的成员变量类型和成员方法的参数类型或返回值类型。

泛型方法

泛型方法也比较简单,在调用方法的时候确定泛型类型,对于参数中的泛型类的参数,可用通配符来定义该泛型类的泛型,也可以用具体的泛型标识,加在返回类型前。

1
2
3
4
public void test(Class<?> cls) {}
public <T> void test(Class <T> cls) {} // 与<?>类似,但方法体中可用标识T,如T t = cls.newInstance();
public <T> void test(T t) {}
public <T,E> void test(T t, E e) {} // 多个泛型标识也可以

但是,使用通配符定义其泛型类型的泛型类,不允许对泛型类对象中的内容进行修改,如:

1
2
3
public void test(List<?> list) {
list.add(1); // 编译报错
}

参数类List的泛型是未知的,而方法体中对该类实例的与泛型类型相关操作是有具体类型的,因而编译器阻止了这一行为。

泛型限制

当我们用<T><?>定义泛型时,对象可以接收任意类作为泛型参数,但有时我们需要限制泛型的“范围”,即从继承的角度来说,允许某个类及其任意子类,或某个类及其任意父类(直至Object类)为泛型类型。

类泛型限制

声明泛型类的泛型上限:[访问权限] [类名] <泛型标识 extends 上限父类>,允许泛型为上限父类或其任意子类。
声明泛型类时不能定义泛型下限。
如某泛型父类的泛型声明为<T><T extends 上限父类>,则子类需继承同样的泛型或缩小泛型范围,同时也可以添加其他泛型类型参数。

1
2
3
4
5
6
7
8
interface A <T> {} // 父类泛型参数为任意类型
class B <T> implements A<T> {} // 子类继承同样的泛型
class C <T extends Number> implements A<T> {} // 子类缩小范围
class D <E, T> implements A<T> {} // 子类添加新的泛型参数

class E <T extends Number> implements C<T> {} // 子类继承同样的泛型
class F <T extends NumberSub> implements C<T> {} // 子类缩小范围
class G <E, T extends Number> implements C<T> {} // 子类添加新的泛型参数

此外,同一个泛型类不同的泛型并不存在继承关系,如:

1
ArrayList<Object> l = new ArrayList<Integer>(); // 编译错误, Object是Integer的父类,但在这并不能强转

方法泛型限制

方法中的泛型限制可用于参数中泛型类的泛型、参数类型或返回值类型,当用于后两者时必须添加泛型标识。

泛型类参数

对于方法的泛型类参数的泛型上下限:[访问权限] [返回类型] [方法名] (泛型类名<? extends 泛型上限父类> 参数名)[访问权限] [返回类型] [方法名] (泛型类名<? super 泛型下限子类> 参数名),此时入参是一个泛型类(如Class类),允许入参的泛型类型是指定的上限父类及其任意子类、或指定的下限父类及其任意父类。
我们也可以用具体的泛型标识来指定:[访问权限] <泛型标识 extends 泛型上限父类> [返回类型] [方法名] (泛型类名<泛型标识> 参数名)[访问权限] <泛型标识 extends 泛型下限子类> [返回类型] [方法名] (泛型类名<泛型标识> 参数名)

1
2
public void test(Class<? extends Number> cls) {} // 参数类型为泛型类Class,其泛型类型为Number或任意子类
public void test(Class<? super Integer> cls) {} // 参数类型为泛型类Class,其泛型类型为Integer或任意父类,直至Object
泛型参数

方法中的泛型类参数的上限:[访问权限] <泛型标识 extends 泛型上限父类> [返回类型] [方法名] (泛型标识 参数名)[访问权限] <泛型标识 super 泛型下限子类> [返回类型] [方法名] (泛型标识 参数名),此时入参就是个泛型,允许其类型是指定的上限父类及其任意子类、或指定的下限父类及其任意父类。

1
2
public <T extends Number> T test(T t) {} // 参数类型为Number或任意子类
public <T super Number> T test(T t) {} // 参数类型为Number或任意父类,直至Object

还有一种情形是既有泛型类入参,又有泛型入参或返回值,且泛型相同,此时需要将泛型标识声明放在返回值类型前:

1
public <T extends Number> T f(Class <T> cls) { return null; }

类型擦除

对于泛型,编译器会如何处理,字节码中是否仍然存在泛型?
一个简单的例子就能说明问题:

1
2
3
List <String> sL = new ArrayList<>();
List <Integer> iL = new ArrayList<>();
System.out.println(sL.getClass() == iL.getClass());

结果为true,在运行阶段,sL的类与iL的类已经相同了,它们在JVM中的类都是List.Class,并不存在List<String>.ClassList<Integer>.Class

那么泛型类对象指定的泛型哪去了?我们可以通过下面的例子观察:

1
2
3
4
5
6
class Test1 <T> {
T t;
}
class Test2 <T extends Integer> {
T t;
}

通过反射我们可以观察到t的类型:

1
2
3
4
5
6
7
8
9
10
Test1<String> test1 = new Test1<String>("hello");
Test2<Integer> test2 = new Test2<String>(1);
Class cls1 = test1.getClass();
Class cls2 = test2.getClass();
System.out.println("Class1=" + cls1.getName());
System.out.println("Class2=" + cls2.getName());
Field[] fields1 = cls1.getDeclaredFields();
Field[] fields2 = cls2.getDeclaredFields();
System.out.println("Field1 " + fields1[0].getName() + " type=" + fields1[0].getType().getName());
System.out.println("Field2 " + fields2[0].getName() + " type=" + fields2[0].getType().getName());

输出为:

1
2
3
4
Class1=....Test1
Class2=....Test2
Field1 t type=Object
Field2 t type=Integer

因此可以得出结论,类型擦除后,<T>变成了Object类型,而<T extends Integer>变成了类型上限Integer。

泛型数组

当我们使用泛型类的数组时,情况有些不同,Java不允许创建一个明确的泛型类型的数组。

1
2
3
ArrayList<String>[] lists = new ArrayList<String>[10]; // 不允许
ArrayList<?>[] lists = new ArrayList<?>[10]; // 允许,但不能进行类型相关的操作,如添加
ArrayList<?>[] lists = new ArrayList[10]; // 允许,但不能进行类型相关的操作,如添加

如下官方的例子:

1
2
3
4
5
6
7
List<String>[] lsa = new List<String>[10]; // 并不能真正通过编译,此处仅作假设说明   
Object o = lsa; // List<String>[] -> Object
Object[] oa = (Object[]) o; // Object -> Object[]
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // 不太对劲,但能通过检查, Object[1] -> ArrayList<Integer>
String s = lsa[1].get(0); // 运行时class cast error

由于类型擦除,在运行时JVM不知道泛型,所以给oa[1]赋值ArrayList不会异常,但取数据时要做类型转换,从而产生ClassCastException。如果允许进行泛型数组的声明,在编译期将不会出现任何警告和错误,而在运行时出错。而对泛型数组的声明进行限制,在编译期提示代码有类型安全问题,多少有点预防作用。

用通配符的声明方式是允许的,因为对于通配符的方式,取数据时要做显式的类型转换。

1
2
3
4
5
6
7
List<?>[] lsa = new List<?>[10]; // 编译允许   
Object o = lsa; // List<String>[] -> Object
Object[] oa = (Object[]) o; // Object -> Object[]
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Object[1] -> ArrayList<Integer>
Integer i = (Integer) lsa[1].get(0); // 显示类型强转,不会报错

Java 7 & 8

泛型在Java 7-,Java 7和Java 8中有些不同。

Java 7-

在Java 7之前,使用泛型实例化时,必须同时指明实例和声明的引用的泛型。

1
Map<String,Integer> map = new HashMap<String, Integer>();

Java 7

从Java 7开始语法上支持type inference,即类型推导(所谓钻石符号),底层是编译器的支持。
类型推导可省掉实例的泛型声明,从声明的引用中获取。

1
Map<String, Integer> map = new HashMap<>();

但这个类型推断仍然是个半成品,因为只有在构造器中可以推导,而在方法调用时不行,如下:

1
2
3
List<String> list = new ArrayList<>();
list.add("Hello World");
list.addAll(new ArrayList<>()); // 编译报错,参数期望Collection<? extends String>,但无法推导新的ArrayList实例的泛型

Java 8+

从Java 8开始,允许方法调用中的类型推导,因此上例中的代码可以通过编译,同时还允许多种推导。

泛型方法

还有另外一个例子可以说明Java 8中泛型的推导:

1
2
<T> T pick(T a1, T a2) { return a2; }
Serializable s = obj.pick("d", new ArrayList<String>());

初看是不是有点奇怪,同样的泛型标识居然传了一个String和一个泛型为Integer的ArrayList对象,但其实他们都实现了Serializable接口。而对于方法参数的泛型,是根据接收返回值的引用声明的类型来推导的。
在之前的版本这一层推导是无法完成的,需要在代码中指明:

1
Serializable s = obj.<Serializable>pick("d", new ArrayList<String>());

泛型构造方法

另外,泛型类及非泛型类均能使用泛型构造方法的推导:

1
2
3
4
5
6
class NonGeneric {
<T> NonGeneric (T t) {}
}
class Generic<E> {
<T> Generic (T t) {}
}

对于泛型及非泛型类,可以使用泛型构造方法,以及相应的推导。

1
2
NonGeneric ng = new NonGeneric(1);  // 构造方法的泛型为Integer
Generic<Integer> g = new Generic(""); // 构造方法的泛型为String,类的泛型为Integer

目标类型

方法的泛型还能根据返回值的接收引用声明的泛型类型来推导,这在Java 7和8中通用:

1
2
<T> List<T> test() {}
List<String> list = obj.test(); // 根据接收返回值的引用声明的泛型类型String,推导出方法中的泛型为String

但在Java 7中,下面这种嵌套的方法调用时就需要注意了:

1
2
void test2(List<String> list) {}
obj.test2(obj.test()); // Java 7不允许,只能obj.test2(obj.<String>test());

在Java 7中,因为test返回的类型是List<T>,未能推导出List<String>,泛型未指定具体类型,因此编译器将其泛型设为Object,从而传入test2的参数是List<Object>,显然test2期待的是List<String>,所以编译不通过。
而在Java 8,因为能推导出test返回类型的是List<String>,从而编译通过。